Skip to content

fix(xiaohongshu): hook dashboard fetch to capture signed datacenter/note/* responses#1732

Merged
jackwener merged 5 commits into
jackwener:mainfrom
Benjamin-eecs:fix/xhs-creator-detail-signed-api
May 25, 2026
Merged

fix(xiaohongshu): hook dashboard fetch to capture signed datacenter/note/* responses#1732
jackwener merged 5 commits into
jackwener:mainfrom
Benjamin-eecs:fix/xhs-creator-detail-signed-api

Conversation

@Benjamin-eecs
Copy link
Copy Markdown
Contributor

@Benjamin-eecs Benjamin-eecs commented May 23, 2026

Description

The four /api/galaxy/creator/datacenter/note/* endpoints behind the creator-note-detail view require an x-s / x-t / x-s-common signing interceptor that the dashboard's own JS installs at page load. The previous in-page roundtrip called fetch() directly from page.evaluate, which bypasses the interceptor and gets HTTP 406, so 观看来源 / 观众画像 / 趋势数据 rows silently never landed even though the help string promised them.

Instead of forging signatures, install a fetch + XHR capture hook on window.__xhsCapture, SPA-navigate to /statistics/note-detail via history.pushState + popstate (a hard page.goto would wipe the hook before the first auto-fetch fires), and harvest the dashboard's own signed responses out of the capture buffer.

Also fix a 1-character endpoint name: /note/audience is renamed to /note/audience/source. The old path returned 404 even when signed; the page actually fetches /note/audience/source for the 观看来源 panel. Confirmed against the live dashboard XHR list while logged in.

Tests updated to mock the new install-hook + SPA-nav + poll-capture sequence at page.evaluate (the previous burst-wait-between-fetches assertion no longer applies).

Related issue: Closes #1728. Reporter diagnosis: @ppop123 traced the signing bypass + endpoint typo and verified the hook + SPA-nav workaround on 86 notes.

Type of Change

  • 🐛 Bug fix
  • ✨ New feature
  • 🌐 New site adapter
  • 📝 Documentation
  • ♻️ Refactor
  • 🔧 CI / build / tooling

Checklist

  • I ran the checks relevant to this PR
  • I updated tests or docs if needed
  • I included output or screenshots when useful

Documentation (if adding/modifying an adapter)

  • Added doc page under docs/adapters/ (if new adapter)
  • Updated docs/adapters/index.md table (if new adapter)
  • Updated sidebar in docs/.vitepress/config.mts (if new adapter)
  • Updated README.md / README.zh-CN.md when command discoverability changed
  • Used positional args for the command's primary subject unless a named flag is clearly better
  • Normalized expected adapter failures to CliError subclasses instead of raw Error

Screenshots / Output

Mechanism verification (broader XHR hook installed on the live dashboard for diagnosis, on a published test note):

fetch / XHR captured after history.pushState('/statistics/note-detail?noteId=...') + dispatchEvent(popstate):
- xhr https://creator.xiaohongshu.com/api/galaxy/creator/datacenter/note/base?note_id=...
- xhr https://creator.xiaohongshu.com/api/galaxy/creator/datacenter/note/analyze/audience/trend?note_id=...
- xhr https://creator.xiaohongshu.com/api/galaxy/creator/datacenter/note/audience/source?note_id=...

All 4 signed XHRs fire and the hook captures their responses; the typo-fixed /note/audience/source matches what the dashboard actually serves.

End-to-end on a freshly published test note (id 6a1195a8..., 0 views so most metrics return placeholders):

$ opencli xiaohongshu creator-note-detail 6a1195a8000000000702f181
- section: 笔记信息       metric: note_id        value: 6a1195a8000000000702f181
- section: 笔记信息       metric: title          value: 'OpenCLI #1728 fix verification ...'
- section: 笔记信息       metric: published_at   value: 2026-05-23 20:55
- section: 基础数据       metric: 曝光数         value: '-'   extra: '-'
- section: 基础数据       metric: 观看数         value: '0'   extra: '-'
- section: 互动数据       metric: 点赞数         value: '0'   extra: '-'
- section: 互动数据       metric: 评论数         value: '0'   extra: '-'
- section: 互动数据       metric: 收藏数         value: '0'   extra: '-'
- section: 互动数据       metric: 分享数         value: '0'   extra: '-'
- section: 趋势说明       metric: 观众趋势       value: 暂不可用   extra: 观看数不足100,暂时无法分析

The 趋势说明 row at the bottom is the diagnostic signal: it is produced by appendTrendRows against a non-null apiPayload, so the API response was captured and parsed. Without this fix, apiPayload === null (silenced 406) and that section was completely absent.

Note: the trend / audience source / audience portrait sections still need ≥100 views to surface real breakdown rows (xhs platform rule), which @ppop123 verified end-to-end on 86-148 note accounts in #1728. The mechanism is unblocked here; the dataset depth depends on the target account.

@Benjamin-eecs Benjamin-eecs marked this pull request as ready for review May 23, 2026 12:04
Copilot AI review requested due to automatic review settings May 23, 2026 12:04
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes missing audience-source / audience-portrait / trend sections in the xiaohongshu creator-note-detail adapter by capturing the creator dashboard’s own signed /api/galaxy/creator/datacenter/note/* responses (instead of issuing unsigned fetch() calls from page.evaluate), and corrects an endpoint suffix typo.

Changes:

  • Update the audience source endpoint suffix to /api/galaxy/creator/datacenter/note/audience/source.
  • Install a window.fetch + XMLHttpRequest capture hook and SPA-navigate via history.pushState + popstate to harvest signed dashboard responses.
  • Update tests to mock the new hook + SPA-nav + polling capture flow.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
clis/xiaohongshu/creator-note-detail.js Switch from direct API fetch() to capture-hook + SPA navigation; fix the audience source endpoint suffix.
clis/xiaohongshu/creator-note-detail.test.js Update mocks/assertions to reflect hook installation and capture-buffer polling behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +255 to +258
await page.evaluate(`(() => {
if (window.__xhsCapture) return;
window.__xhsCapture = {};
const origFetch = window.fetch;
Comment on lines +289 to +290
HookedXHR.prototype = OrigXHR.prototype;
window.XMLHttpRequest = HookedXHR;
…ote/* responses

The four /api/galaxy/creator/datacenter/note/* endpoints behind the
creator-note-detail view require an x-s / x-t / x-s-common signing
interceptor that the dashboard's own JS installs at page load. The
previous in-page roundtrip called fetch() directly from page.evaluate,
which bypasses the interceptor and gets HTTP 406, so 观看来源 / 观众画像 /
趋势数据 rows silently never landed even though the help string promised
them.

Instead of forging signatures, install a fetch + XHR capture hook on
window.__xhsCapture, SPA-navigate to /statistics/note-detail via
history.pushState + popstate (a hard page.goto would wipe the hook
before the first auto-fetch fires), and harvest the dashboard's own
signed responses out of the capture buffer.

Also fix a 1-character endpoint name: /note/audience -> /note/audience/source.
The old path returned 404 even when signed; the page actually fetches
/note/audience/source for the 观看来源 panel. Confirmed against the live
dashboard XHR list while logged in.

Tests updated to mock the new install-hook + SPA-nav + poll-capture
sequence at page.evaluate (the previous burst-wait-between-fetches
assertion no longer applies).

Closes jackwener#1728.

Reporter diagnosis: @ppop123 traced the signing bypass + endpoint typo
and verified the hook + SPA-nav workaround on 86 notes.
…ibling tone

Sibling helper functions in creator-note-detail.js have no doc-comment
block above the declaration; the 5-line WHY block on the new hook was
out of style. Compress to two lines covering the same WHY (signed API
bypass + 406) and let the rest of the context live in the commit body
of the parent fix.
Inline literals (20 iteration cap, 0.5s wait) drift from sibling
convention in clis/xiaohongshu/delete-note.js where the same kind of
post-write polling is named VERIFY_TIMEOUT_MS / VERIFY_POLL_MS. Promote
the two values to CAPTURE_POLL_ATTEMPTS / CAPTURE_POLL_INTERVAL_S so
the loop reads against an explicit budget and future tuning lands in
one place.
@Benjamin-eecs Benjamin-eecs force-pushed the fix/xhs-creator-detail-signed-api branch from 52bf2c4 to f636191 Compare May 23, 2026 16:16
Two polish items from the Copilot review on jackwener#1732:

- Buffer reset: window.__xhsCapture is now cleared on every install call
  so stale captures from a previous run on the same tab cannot leak into
  the current navigation's harvest. The wrapper-install guard moves to a
  separate __xhsCaptureInstalled flag so the fetch/XHR monkey-patches
  themselves are still installed exactly once per page lifetime.
- XHR static constants: HookedXHR now copies the readyState constants
  (UNSENT / OPENED / HEADERS_RECEIVED / LOADING / DONE) from the original
  constructor so dashboard code that reads XMLHttpRequest.DONE etc against
  the constructor keeps working.
@jackwener jackwener force-pushed the fix/xhs-creator-detail-signed-api branch from f077ac7 to b02e7e3 Compare May 25, 2026 06:27
@jackwener jackwener merged commit 6b8d30b into jackwener:main May 25, 2026
11 checks passed
Benjamin-eecs added a commit to Benjamin-eecs/OpenCLI that referenced this pull request May 25, 2026
…st cap

The /api/galaxy/creator/datacenter/note/analyze/list endpoint serves 10
note rows per page, and the previous fetchCreatorNotesByApi only ever
requested page 1 because the in-page direct fetch() bypassed xhs's
signing interceptor and returned HTTP 406 for subsequent pages. As a
result `opencli xiaohongshu creator-notes --limit 25` silently capped
at 10 even for accounts with hundreds of notes.

Install the same window.__xhsCapture fetch + XHR hook used by
creator-note-detail (jackwener#1732), SPA-navigate to /statistics/data-analysis
so the dashboard fires its own signed page_num=1 request under the
hook, then click .d-pagination-page buttons for pages 2..N to make the
dashboard's React router fire successive signed requests. Dedupe by
note.id and return up to --limit.

Pagination buttons render the page number duplicated in textContent
("22" for page 2 because of an inner accessibility span + visible
span), so the click selector tolerates both the raw digit and the
doubled form. CAPTURE_POLL_ATTEMPTS / CAPTURE_POLL_INTERVAL_S match
the constant naming used by sibling delete-note.js.

Fresh notes whose title field is still empty in the API response get
enriched from the note-manager card DOM (which derives a title from the
content's first line), so the pre-existing title coverage is preserved
for the rows the API surfaces empty.

Live-verified on benjamin-eecs's 圣诞薯 account (11 published notes,
data-analysis permission active): creator-notes --limit 15 now returns
all 11 rows, with 10 titles enriched via note-manager and 1 left empty
because that note is older than note-manager's first 10 visible cards.
For real-world use (e.g. @ppop123's reported 148-note account), all
titles populate directly from the API.

Closes jackwener#1729.
Benjamin-eecs added a commit to Benjamin-eecs/OpenCLI that referenced this pull request May 26, 2026
…st cap

The /api/galaxy/creator/datacenter/note/analyze/list endpoint serves 10
note rows per page, and the previous fetchCreatorNotesByApi only ever
requested page 1 because the in-page direct fetch() bypassed xhs's
signing interceptor and returned HTTP 406 for subsequent pages. As a
result `opencli xiaohongshu creator-notes --limit 25` silently capped
at 10 even for accounts with hundreds of notes.

Install the same window.__xhsCapture fetch + XHR hook used by
creator-note-detail (jackwener#1732), SPA-navigate to /statistics/data-analysis
so the dashboard fires its own signed page_num=1 request under the
hook, then click .d-pagination-page buttons for pages 2..N to make the
dashboard's React router fire successive signed requests. Dedupe by
note.id and return up to --limit.

Pagination buttons render the page number duplicated in textContent
("22" for page 2 because of an inner accessibility span + visible
span), so the click selector tolerates both the raw digit and the
doubled form. CAPTURE_POLL_ATTEMPTS / CAPTURE_POLL_INTERVAL_S match
the constant naming used by sibling delete-note.js.

Fresh notes whose title field is still empty in the API response get
enriched from the note-manager card DOM (which derives a title from the
content's first line), so the pre-existing title coverage is preserved
for the rows the API surfaces empty.

Live-verified on benjamin-eecs's 圣诞薯 account (11 published notes,
data-analysis permission active): creator-notes --limit 15 now returns
all 11 rows, with 10 titles enriched via note-manager and 1 left empty
because that note is older than note-manager's first 10 visible cards.
For real-world use (e.g. @ppop123's reported 148-note account), all
titles populate directly from the API.

Closes jackwener#1729.
Benjamin-eecs added a commit to Benjamin-eecs/OpenCLI that referenced this pull request May 26, 2026
…st cap

The /api/galaxy/creator/datacenter/note/analyze/list endpoint serves 10
note rows per page, and the previous fetchCreatorNotesByApi only ever
requested page 1 because the in-page direct fetch() bypassed xhs's
signing interceptor and returned HTTP 406 for subsequent pages. As a
result `opencli xiaohongshu creator-notes --limit 25` silently capped
at 10 even for accounts with hundreds of notes.

Install the same window.__xhsCapture fetch + XHR hook used by
creator-note-detail (jackwener#1732), SPA-navigate to /statistics/data-analysis
so the dashboard fires its own signed page_num=1 request under the
hook, then click .d-pagination-page buttons for pages 2..N to make the
dashboard's React router fire successive signed requests. Dedupe by
note.id and return up to --limit.

Pagination buttons render the page number duplicated in textContent
("22" for page 2 because of an inner accessibility span + visible
span), so the click selector tolerates both the raw digit and the
doubled form. CAPTURE_POLL_ATTEMPTS / CAPTURE_POLL_INTERVAL_S match
the constant naming used by sibling delete-note.js.

Fresh notes whose title field is still empty in the API response get
enriched from the note-manager card DOM (which derives a title from the
content's first line), so the pre-existing title coverage is preserved
for the rows the API surfaces empty.

Live-verified on benjamin-eecs's 圣诞薯 account (11 published notes,
data-analysis permission active): creator-notes --limit 15 now returns
all 11 rows, with 10 titles enriched via note-manager and 1 left empty
because that note is older than note-manager's first 10 visible cards.
For real-world use (e.g. @ppop123's reported 148-note account), all
titles populate directly from the API.

Closes jackwener#1729.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] xiaohongshu creator-note-detail returns only 3 sections — 观看来源/观众画像/趋势数据 missing despite help string promising them

3 participants